package org.andengine.util.preferences;

import android.annotation.TargetApi;
import android.content.SharedPreferences;
import android.os.Build;
import android.util.Base64;

import org.andengine.util.exception.MethodNotYetImplementedException;
import org.andengine.util.preferences.exception.SecureSharedPreferencesException;

import java.io.UnsupportedEncodingException;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;

/**
 * (c) 2013 Nicolas Gramlich
 *
 * @author Nicolas Gramlich
 * @since 20:09:38 - 13.04.2013
 */
public class SecureSharedPreferences implements SharedPreferences {
    // ===========================================================
    // Constants
    // ===========================================================

    protected static final String CIPHER_TRANSFORMATION = "AES/CBC/PKCS5Padding";
    protected static final String KEY_HASH_TRANSFORMATION = "SHA-256";
    protected static final String CHARSET = "UTF-8";

    // ===========================================================
    // Fields
    // ===========================================================

    protected final SharedPreferences mDelegate;

    protected final boolean mEncryptKeys;
    protected final boolean mEncryptValues;

    protected final Cipher mEncryptCipher;
    protected final Cipher mDecryptCipher;

    protected final ReadWriteLock mReadWriteLock = new ReentrantReadWriteLock(true);

    // ===========================================================
    // Constructors
    // ===========================================================

    public SecureSharedPreferences(final SharedPreferences pDelegate, final String pSecureKey) throws SecureSharedPreferencesException {
        this(pDelegate, pSecureKey, true, true);
    }

    public SecureSharedPreferences(final SharedPreferences pDelegate, final String pSecureKey, final boolean pEncryptKeys, final boolean pEncryptValues) throws SecureSharedPreferencesException {
        this.mDelegate = pDelegate;
        this.mEncryptKeys = pEncryptKeys;
        this.mEncryptValues = pEncryptValues;
        try {
            this.mEncryptCipher = Cipher.getInstance(SecureSharedPreferences.CIPHER_TRANSFORMATION);
            this.mDecryptCipher = Cipher.getInstance(SecureSharedPreferences.CIPHER_TRANSFORMATION);

            final IvParameterSpec ivSpec = this.getIvParameterSpec();
            final SecretKeySpec secretKey = this.getSecretKeySpec(pSecureKey);

            this.mEncryptCipher.init(Cipher.ENCRYPT_MODE, secretKey, ivSpec);
            this.mDecryptCipher.init(Cipher.DECRYPT_MODE, secretKey, ivSpec);
        } catch (final GeneralSecurityException e) {
            throw new SecureSharedPreferencesException(e);
        } catch (final UnsupportedEncodingException e) {
            throw new SecureSharedPreferencesException(e);
        }
    }

    // ===========================================================
    // Getter & Setter
    // ===========================================================

    protected static byte[] crypt(final Cipher pCipher, final byte[] pBytes) throws SecureSharedPreferencesException {
        try {
            return pCipher.doFinal(pBytes);
        } catch (final Exception e) {
            throw new SecureSharedPreferencesException(e);
        }
    }

    public Lock getReadLock() {
        return this.mReadWriteLock.readLock();
    }

    // ===========================================================
    // Methods for/from SuperClass/Interfaces
    // ===========================================================

    public Lock getWriteLock() {
        return this.mReadWriteLock.writeLock();
    }

    @Override
    public Editor edit() {
        return new Editor();
    }

    @Override
    public Map<String, ?> getAll() {
        throw new MethodNotYetImplementedException();
    }

    @Override
    public boolean getBoolean(final String pKey, final boolean pDefaultValue) {
        final String encryptedKey = this.encryptKey(pKey);
        final String value = this.mDelegate.getString(encryptedKey, null);
        if (value == null) {
            return pDefaultValue;
        } else {
            final String decryptedValue = this.decryptValue(value);
            return Boolean.parseBoolean(decryptedValue);
        }
    }

    @Override
    public int getInt(final String pKey, final int pDefaultValue) {
        final String encryptedKey = this.encryptKey(pKey);
        final String value = this.mDelegate.getString(encryptedKey, null);
        if (value == null) {
            return pDefaultValue;
        } else {
            final String decryptedValue = this.decryptValue(value);
            return Integer.parseInt(decryptedValue);
        }
    }

    @Override
    public long getLong(final String pKey, final long pDefaultValue) {
        final String encryptedKey = this.encryptKey(pKey);
        final String value = this.mDelegate.getString(encryptedKey, null);
        if (value == null) {
            return pDefaultValue;
        } else {
            final String decryptedValue = this.decryptValue(value);
            return Long.parseLong(decryptedValue);
        }
    }

    @Override
    public float getFloat(final String pKey, final float pDefaultValue) {
        final String encryptedKey = this.encryptKey(pKey);
        final String value = this.mDelegate.getString(encryptedKey, null);
        if (value == null) {
            return pDefaultValue;
        } else {
            final String decryptedValue = this.decryptValue(value);
            return Float.parseFloat(decryptedValue);
        }
    }

    @Override
    public String getString(final String pKey, final String pDefaultValue) {
        final String encryptedKey = this.encryptKey(pKey);
        final String value = this.mDelegate.getString(encryptedKey, null);
        if (value == null) {
            return pDefaultValue;
        } else {
            final String decryptedValue = this.decryptValue(value);
            return decryptedValue;
        }
    }

    @Override
    public boolean contains(final String pKey) {
        return this.mDelegate.contains(pKey);
    }

    @Override
    public void registerOnSharedPreferenceChangeListener(final OnSharedPreferenceChangeListener pOnSharedPreferenceChangeListener) {
        this.mDelegate.registerOnSharedPreferenceChangeListener(pOnSharedPreferenceChangeListener);
    }

    @Override
    public void unregisterOnSharedPreferenceChangeListener(final OnSharedPreferenceChangeListener pOnSharedPreferenceChangeListener) {
        this.mDelegate.unregisterOnSharedPreferenceChangeListener(pOnSharedPreferenceChangeListener);
    }

    // ===========================================================
    // Methods
    // ===========================================================

    @Override
    public Set<String> getStringSet(final String pKey, final Set<String> pStringSet) {
        throw new MethodNotYetImplementedException();
    }

    protected String encryptKey(final String pKey) {
        if (this.mEncryptKeys) {
            return this.encrypt(pKey);
        } else {
            return pKey;
        }
    }

    protected String encryptValue(final String pValue) {
        if (this.mEncryptValues) {
            return this.encrypt(pValue);
        } else {
            return pValue;
        }
    }

    protected String decryptKey(final String pKey) {
        if (this.mEncryptKeys) {
            return this.decrypt(pKey);
        } else {
            return pKey;
        }
    }

    protected String decryptValue(final String pValue) {
        if (this.mEncryptValues) {
            return this.decrypt(pValue);
        } else {
            return pValue;
        }
    }

    protected IvParameterSpec getIvParameterSpec() {
        final int blockSize = this.mEncryptCipher.getBlockSize();
        final byte[] iv = new byte[blockSize];
        System.arraycopy("abcdefghijklmnopqrstuvwxzyABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789".getBytes(), 0, iv, 0, blockSize);
        return new IvParameterSpec(iv);
    }

    protected SecretKeySpec getSecretKeySpec(final String pKey) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        final byte[] keyBytes = this.createKeyBytes(pKey);
        return new SecretKeySpec(keyBytes, SecureSharedPreferences.CIPHER_TRANSFORMATION);
    }

    protected byte[] createKeyBytes(final String pKey) throws UnsupportedEncodingException, NoSuchAlgorithmException {
        final MessageDigest md = MessageDigest.getInstance(SecureSharedPreferences.KEY_HASH_TRANSFORMATION);
        md.reset();
        final byte[] keyBytes = md.digest(pKey.getBytes(SecureSharedPreferences.CHARSET));
        return keyBytes;
    }

    protected String encrypt(final String pPlainText) throws SecureSharedPreferencesException {
        byte[] secureValue;
        try {
            secureValue = SecureSharedPreferences.crypt(this.mEncryptCipher, pPlainText.getBytes(SecureSharedPreferences.CHARSET));
        } catch (final UnsupportedEncodingException e) {
            throw new SecureSharedPreferencesException(e);
        }
        final String secureValueEncoded = Base64.encodeToString(secureValue, Base64.NO_WRAP);
        return secureValueEncoded;
    }

    protected String decrypt(final String pCipherText) {
        final byte[] securedValue = Base64.decode(pCipherText, Base64.NO_WRAP);
        final byte[] pValue = SecureSharedPreferences.crypt(this.mDecryptCipher, securedValue);
        try {
            return new String(pValue, SecureSharedPreferences.CHARSET);
        } catch (final UnsupportedEncodingException e) {
            throw new SecureSharedPreferencesException(e);
        }
    }

    // ===========================================================
    // Inner and Anonymous Classes
    // ===========================================================

    public class Editor implements SharedPreferences.Editor {
        // ===========================================================
        // Constants
        // ===========================================================

        // ===========================================================
        // Fields
        // ===========================================================

        protected final SharedPreferences.Editor mDelegate;

        // ===========================================================
        // Constructors
        // ===========================================================

        public Editor() {
            this.mDelegate = SecureSharedPreferences.this.mDelegate.edit();
        }

        // ===========================================================
        // Getter & Setter
        // ===========================================================

        // ===========================================================
        // Methods for/from SuperClass/Interfaces
        // ===========================================================

        @Override
        public Editor putBoolean(final String pKey, final boolean pValue) {
            final String encryptedKey = SecureSharedPreferences.this.encryptKey(pKey);
            final String encryptedValue = SecureSharedPreferences.this.encryptValue(Boolean.toString(pValue));
            this.mDelegate.putString(encryptedKey, encryptedValue);
            return this;
        }

        @Override
        public Editor putInt(final String pKey, final int pValue) {
            final String encryptedKey = SecureSharedPreferences.this.encrypt(pKey);
            final String encryptValue = SecureSharedPreferences.this.encryptValue(Integer.toString(pValue));
            this.mDelegate.putString(encryptedKey, encryptValue);
            return this;
        }

        @Override
        public Editor putLong(final String pKey, final long pValue) {
            final String encryptedKey = SecureSharedPreferences.this.encrypt(pKey);
            final String encryptValue = SecureSharedPreferences.this.encryptValue(Long.toString(pValue));
            this.mDelegate.putString(encryptedKey, encryptValue);
            return this;
        }

        @Override
        public Editor putFloat(final String pKey, final float pValue) {
            final String encryptedKey = SecureSharedPreferences.this.encrypt(pKey);
            final String encryptedVaue = SecureSharedPreferences.this.encryptValue(Float.toString(pValue));
            this.mDelegate.putString(encryptedKey, encryptedVaue);
            return this;
        }

        @Override
        public Editor putString(final String pKey, final String pValue) {
            final String encryptedKey = SecureSharedPreferences.this.encrypt(pKey);
            final String encryptValue = SecureSharedPreferences.this.encryptValue(pValue);
            this.mDelegate.putString(encryptedKey, encryptValue);
            return this;
        }

        @Override
        public Editor putStringSet(final String pKey, final Set<String> pStringSet) {
            throw new MethodNotYetImplementedException();
        }

        @Override
        public Editor remove(final String pKey) {
            final String encryptedKey = SecureSharedPreferences.this.encrypt(pKey);
            this.mDelegate.remove(encryptedKey);
            return this;
        }

        @Override
        public Editor clear() {
            this.mDelegate.clear();
            return this;
        }

        @Override
        public boolean commit() {
            return this.mDelegate.commit();
        }

        @TargetApi(Build.VERSION_CODES.GINGERBREAD)
        @Override
        public void apply() {
            this.mDelegate.apply();
        }

        // ===========================================================
        // Methods
        // ===========================================================

        // ===========================================================
        // Inner and Anonymous Classes
        // ===========================================================
    }
}